#!/usr/local/bin/python3
# -*- coding: utf-8 -*-

"""
@File    : li.py
@Author  : Link
@Time    : 2022/12/23 20:35
@Mark    : 
"""
import os
import pickle
from multiprocessing import Process
from typing import List, Dict, Union, Tuple

import numpy as np
import pandas as pd
from PySide2.QtCore import QObject, Signal

from app_test.test_utils.wrapper_utils import Time
from common.app_variable import DataModule, ToChartCsv, GlobalVariable, PtmdModule, LimitType, FailFlag, PartFlags, \
    ReadFail
from common.cal_interface.capability import CapabilityUtils
from common.data_class_interface.for_analysis_stdf import PTMD_HEAD, DTP_HEAD, STDF_HEAD, PRR_HEAD, BIN_HEAD
from common.data_class_interface.for_calculation import Calculation
from parser_core.stdf_parser_file_write_read import ParserData
from parser_core.stdf_parser_func import PtmdOptFlag, PtmdParmFlag
from report_core.openxl_utils.utils import OpenXl
from ui_component.ui_app_variable import UiGlobalVariable
from ui_component.ui_common.my_text_browser import Print


class SummaryCore:
    """
    1. 每个文件解析完成后, 会有各个Summary和子数据
    2. Summary会经过组合后生成 SummaryDf
        SummaryDf -> by start_time 排序
        | ID  | R | LOT_ID | SB_LOT_ID | FLOW_ID | QTY | PASS | YIELD | PASS_VERIFY | ... |
        | ··· | Y | ······ |
        | ··· | N |
        | ···

        df_dict ->
        {
            逻辑需要优化, 数据要在需要的时候才从HDF5中读取·
        }
    3. 组合新的Limit数据,注意测试项目和TEST_ID的对应即可
    4. SummaryDf 展示在Tree上
        Group By: LOT_ID -> FLOW_ID ? -> SB_LOT_ID ?
            groupby之后 使用min(START_TIME), max(FINISH_TIME), sum(QTY), sum(PASS)
        给到Tree的数据:
            [
                {
                | ID  | LOT_ID | ...
                children:
                    [ {
                    | ID  | LOT_ID | ...
                    }, ... ]
                },
                {
                | ID  | LOT_ID | ...
                children:
                    [ {
                    | ID  | LOT_ID | ...
                    }, ... ]
                }
            ]
    5. 从Tree中拿到IDS, 汇整为NowSummaryDf, 并拿到Group信息后汇整为 GROUP列
        会有两份数据, 1. NowSummaryDf 2. NowDfs->将df_dict中的数据按需求contact起来
    6. 支持多个window来汇整数据
    """
    ready: bool = False
    summary_df: pd.DataFrame = None

    def set_data(self, summary: Union[list, pd.DataFrame]):
        if summary is None:
            return
        if isinstance(summary, list):
            self.summary_df = pd.DataFrame(summary)
        else:
            self.summary_df = summary
        self.ready = True
        return self.ready

    def get_summary_tree(self):
        """
        SummaryDf 展示在Tree上, 这个可以从本地或是Web存储中将数据读取回来
        :return:
        """
        tree_dict_list = list()
        for (g_type, key), e_df in self.summary_df.groupby(
                [STDF_HEAD.GROUP_TYPE, STDF_HEAD.GROUP_TEXT]):  # type:str, pd.DataFrame
            key = str(key)
            qty = e_df[STDF_HEAD.QTY].sum()
            pass_qty = e_df[STDF_HEAD.PASS].sum()
            if qty == 0:
                pass_yield = "0.0%"
            else:
                pass_yield = '{}%'.format(round(pass_qty / qty * 100, 2))
            tree_dict = {
                STDF_HEAD.GROUP_TYPE: g_type,
                STDF_HEAD.GROUP_TEXT: key,
                STDF_HEAD.QTY: qty,
                STDF_HEAD.PASS: pass_qty,
                STDF_HEAD.YIELD: pass_yield,
                STDF_HEAD.START_T: e_df[STDF_HEAD.START_T].min(),
                "children": e_df.to_dict(orient="records")
            }
            tree_dict_list.append(tree_dict)

        return tree_dict_list

    def add_custom_node(self, ids: List[int], new_lot_id: str):
        """
        将多个数据组合为一个自定义的LOT, 比如两个版本的数据对比, 将一部分分为版本A(LOT_A), 另一部分分为版本B(LOT_B)
        其实没那么复杂, 不需要新建各种五花八门的数据, 就直接把旧的列的LOT_ID改名即可
        {
            'FILE_PATH': '',
            'FILE_NAME': '',
            'ID': 100000,
            'GROUP_TYPE': 'LOT_ID|WAFER_ID',
            'GROUP_TEXT': '',
            'LOT_ID': '',
            'SBLOT_ID': '',
            "WAFER_ID": "",
            "BLUE_FILM_ID": "",
            'TEST_COD': '',
            'FLOW_ID': '',
            'PART_TYP': '',
            'JOB_NAM': '',
            'TST_TEMP': '',
            'NODE_NAM': '',
            'SETUP_T': 1620734064,
            'START_T': 1620734070,
            'SITE_CNT': 1,
            'QTY': 2534,
            'PASS': 2534,
            'YIELD': '100.0%',
            'PART_FLAG': 0,
            'READ_FAIL': True
            'HDF5_PATH': ""
            },
        """
        if self.summary_df is None:
            return
        self.summary_df.loc[self.summary_df.ID.isin(ids), STDF_HEAD.GROUP_TEXT] = new_lot_id

    def load_select_data(self, ids: List[int], quick: bool = False, sample_num: int = 1E4, old=False):
        """
        返回数据
        整理出一个比较完整的 ptmd 的整合dict
        重复的ptmd_dict就选用最新的
        主要给每个单元的Prr给一个ID用于数据链接
        TODO: 不在一个summary中指向多个文件位置
        :param ids:
        :param quick:
        :param sample_num:
        :param old: 旧的数据模型
        :return:
        """
        id_module_dict = {}
        select_summary = self.summary_df[self.summary_df.ID.isin(ids)]
        for select in select_summary.itertuples():
            ID = getattr(select, STDF_HEAD.ID)
            data_module = ParserData.load_hdf5_analysis(
                getattr(select, STDF_HEAD.HDF5_PATH),
                getattr(select, STDF_HEAD.PART_FLAG),
                getattr(select, STDF_HEAD.READ_FAIL),
                unit_id=ID,
                old=old, quick=quick, sample_num=sample_num

            )
            id_module_dict[ID] = data_module
        return select_summary, id_module_dict

    def pre_view_select_data(self, ids: List[int]) -> Union[None, pd.DataFrame]:
        """
        用来先看下Limit, 调用PreView的话, 就只处理好Limit的数据
        """
        select_summary: pd.DataFrame = self.summary_df[self.summary_df.ID.isin(ids)]
        if len(select_summary) == 0:
            return None
        ptmd_df_list: List[pd.DataFrame] = list()
        for select in select_summary.itertuples():
            ID = getattr(select, STDF_HEAD.ID)
            ptmd_df = ParserData.load_ptmd_df(
                getattr(select, STDF_HEAD.HDF5_PATH),
                unit_id=ID,
            )
            if ptmd_df is None:
                Print.Warning("PreView 有数据读取失败/缺失, ID:{}, 是否STDF中没有数据?".format(ID))
            ptmd_df_list.append(ptmd_df)
        # 添加一列为REAL_TEST_ID列作为TEST_ID的数据备份
        if len(ptmd_df_list) == 0:
            return None
        return ParserData.contact_ptmd_list(ptmd_df_list)


class Li(QObject):
    """
    从Tree中得到的确定是需要的数据.
    进到这里面的数据都是数据帧和控制Group的Summary
    """
    select_summary: pd.DataFrame = None
    id_module_dict: Dict[int, DataModule] = None
    df_module: DataModule = None
    # ======================== signal
    QCalculation = Signal()  # 属于重新计算的模型, 运算比较耗费时间
    QMessage = Signal(str)  # 用于全局来调用一个MessageBox, 只做提示
    QStatusMessage = Signal(str)  # 用于全局来调用一个MessageBox, 只做提示

    QChartSelect = Signal()  # 用于刷新选取的数据
    QChartRefresh = Signal()  # 用于重新刷新所有的图
    LimitRefresh = Signal()  # 用于重新刷新LimitTable

    # 用于更新Limit后的数据运算
    # ======================== Temp
    capability_key_list: List[dict] = None  # 计算的制程能力数据
    capability_key_dict: Dict[int, dict] = None  # key: TEST_ID -> 仅仅用于Show Plot
    top_fail_dict: dict = None  # 临时的top fail数据

    # ======================== 用于绘图或是capability group
    to_chart_csv_data: ToChartCsv = None
    group_params = None
    da_group_params = None

    # ReLoadChange
    reload: bool = None  # 用于缓存一些状态, 避免多次重算, =True的时候就需要更新行列矩阵

    def __init__(self):
        super(Li, self).__init__()
        self.reload = False

    @property
    def dt(self):
        return self.to_chart_csv_data.df

    @dt.setter
    def dt(self, new_df: pd.DataFrame):
        if not isinstance(new_df, pd.DataFrame):
            print(f"Error, 必须要赋值和dt一样的数据！···, 错误类型{type(new_df)}")
            return
        self.to_chart_csv_data.dt = new_df

    def __getitem__(self, item):
        return

    def set_data(self,
                 select_summary: pd.DataFrame,
                 id_module_dict: Dict[int, DataModule]
                 ):
        """

        :param select_summary: Mir&Wir等相关的信息整合的Summary
        :param id_module_dict: 每行Summary都有一个唯一ID, 指向了module数据
        :return:
        """
        self.select_summary = select_summary.copy()
        self.select_summary[STDF_HEAD.GROUP] = "*"
        self.id_module_dict = id_module_dict

    def concat(self) -> bool:
        """
        TxODO:
            prr_df.set_index(["PART_ID"])
            dtp_df.set_index(["TEST_ID", "PART_ID"])
            dtp_df.TEST_ID <==> dtp_df.TEST_ID
            prr_df.ID <==> select_summary.ID
        active:
            1. 整合进入数据空间的数据, 都contact成一个数据, 关注["ID", "PART_ID"]这两列
            2. 最终concat成为一份数据, 再做计算就清晰多了
        :return:
        """
        if len(self.id_module_dict) == 0:
            Print.Warning("未选取数据Tree, 无法整合Concat!")
            return False
        data_module_list = []
        for df_id, module in self.id_module_dict.items():
            if module is None:
                Print.Warning("有数据读取失败/缺失, ID:{}, 数据是None, 已经跳过, 是否因STDF中没有数据?".format(df_id))
                continue
            data_module_list.append(module)
        if len(data_module_list) == 0:
            Print.Warning("无有效数据, 无法整合Concat!")
            return False
        self.df_module = ParserData.contact_data_module(data_module_list)
        self.df_module.prr_df.set_index([PRR_HEAD.DIE_ID], inplace=True)
        self.df_module.dtp_df.set_index([PTMD_HEAD.TEST_ID, PRR_HEAD.DIE_ID], inplace=True)
        self.df_module.prr_df[PRR_HEAD.DA_GROUP] = "*"
        self.reload = True
        return True

    def calculation_top_fail(self):
        """
        1. 计算top fail
        2. 需要在unstack的数据格式上
        3. 根据选取的数据来做计算
        :return:
        """
        self.top_fail_dict = CapabilityUtils.calculation_top_fail(self.df_module)

    def calculation_capability(self):
        """
        1. 计算reject rate
        2. 计算cpk等
        :return:
        """
        self.capability_key_list = CapabilityUtils.calculation_capability(self.df_module, self.top_fail_dict)
        if self.capability_key_dict is None:
            self.capability_key_dict = dict()
        else:
            self.capability_key_dict.clear()
        for each in self.capability_key_list:
            self.capability_key_dict[each[PTMD_HEAD.TEST_ID]] = each

    @Time()
    def background_generation_data_use_to_chart_and_to_save_csv(self):
        """
        将数据叠起来, 用于数据可视化和导出到JMP和Altair
        TODO: 数据叠加起来的时候, 会做一个去最后出现的重复项目的操作
        :return:
        """
        if self.to_chart_csv_data is None:
            self.to_chart_csv_data = ToChartCsv()
        if not self.reload:
            Print.Warning("使用内存缓存数据")
            return
        temp_result = self.df_module.dtp_df[[DTP_HEAD.RESULT]]
        temp_result = temp_result[~temp_result.index.duplicated(keep="last")]
        temp_result = temp_result.unstack(0).RESULT  # TODO: 适合处理的位置
        self.to_chart_csv_data.temp_df = temp_result
        self.reload = False

    def background_generation_limit_data_use_to_pat(self):
        """
        用于PAT
        需要提示建议不能在多LOT的Group条件下操作
        :return:
        """
        temp_result = self.df_module.dtp_df[[DTP_HEAD.LO_LIMIT, DTP_HEAD.HI_LIMIT]].copy()
        temp_result = temp_result[~temp_result.index.duplicated(keep="last")]
        self.to_chart_csv_data.limit = temp_result.unstack(0)

    def set_data_group(self, group_params: Union[list, None], da_group_params: Union[list, None]):
        """
        专注将数据分组
        :param group_params:
        :param da_group_params:
        :return:
        """
        if self.select_summary is None:
            return
        if self.df_module is None:
            return
        if self.df_module.prr_df is None:
            return
        self.group_params, self.da_group_params = group_params, da_group_params
        if group_params is None:
            temp_column_data = '*'
        else:
            temp_column_data = None
            for index, each in enumerate(group_params):
                if index == 0:
                    temp_column_data = self.select_summary[each].astype(str)
                else:
                    temp_column_data = temp_column_data + "|" + self.select_summary[each].astype(str)

        self.select_summary.loc[:, STDF_HEAD.GROUP] = temp_column_data
        if da_group_params is None:
            temp_column_data = '*'
        else:
            # prr_df[PRR_HEAD.SITE_NUM] = prr_df[PRR_HEAD.SITE_NUM].apply(lambda x: 'S{:0>3d}'.format(x))
            # DATA_GROUP = [PRR_HEAD.HEAD_NUM, PRR_HEAD.SITE_NUM, PRR_HEAD.HARD_BIN, PRR_HEAD.SOFT_BIN]
            temp_column_data = None
            for index, each in enumerate(da_group_params):
                prr_module = None
                if each == PRR_HEAD.HEAD_NUM:
                    prr_module = self.df_module.prr_df[PRR_HEAD.HEAD_NUM].apply(lambda x: 'H{:0>2d}'.format(x))
                if each == PRR_HEAD.SITE_NUM:
                    prr_module = self.df_module.prr_df[PRR_HEAD.SITE_NUM].apply(lambda x: 'S{:0>2d}'.format(x))
                if each == PRR_HEAD.HARD_BIN:
                    prr_module = self.df_module.prr_df[PRR_HEAD.HARD_BIN].apply(lambda x: 'BH{:0>3d}'.format(x))
                if each == PRR_HEAD.SOFT_BIN:
                    prr_module = self.df_module.prr_df[PRR_HEAD.SOFT_BIN].apply(lambda x: 'BS{:0>3d}'.format(x))
                if prr_module is None:
                    continue
                if index == 0:
                    temp_column_data = prr_module
                else:
                    temp_column_data = temp_column_data + "|" + prr_module
        self.df_module.prr_df.loc[:, PRR_HEAD.DA_GROUP] = temp_column_data

        self.background_generation_data_use_to_chart_and_to_save_csv()
        data = pd.merge(self.to_chart_csv_data.temp_df, self.df_module.prr_df, left_index=True, right_index=True)
        self.to_chart_csv_data.df = pd.merge(
            data, self.select_summary[[STDF_HEAD.ID, STDF_HEAD.GROUP]], on=STDF_HEAD.ID
        )
        self.set_chart_df_group()

    def set_chart_data(self, chart_df: Union[pd.DataFrame, None]):
        """
        用于pyqtgraph绘图
        :param chart_df:
        :return:
        """
        self.to_chart_csv_data.chart_df = chart_df
        if chart_df is None:
            self.select_chart()
            return
        group_data = {}
        for (group, da_group), df in self.to_chart_csv_data.chart_df.groupby([STDF_HEAD.GROUP, PRR_HEAD.DA_GROUP]):
            key = f"{group}@{da_group}"
            group_data[key] = df
        self.to_chart_csv_data.group_chart_df = group_data
        self.select_chart()

    @Time()
    def set_chart_df_group(self):
        group_data = {}
        for (group, da_group), df in self.to_chart_csv_data.df.groupby([STDF_HEAD.GROUP, PRR_HEAD.DA_GROUP]):
            key = f"{group}@{da_group}"
            group_data[key] = df
        self.to_chart_csv_data.group_df = group_data
        self.set_chart_data(None)
        self.refresh_chart()
        return True

    def set_coord(self, coord: str, test_id: int) -> bool:
        """
        coord: PRR_HEAD.X_COORD | PRR_HEAD.Y_COORD
        """
        try:
            self.to_chart_csv_data.df[coord] = self.to_chart_csv_data.df[test_id].fillna(0).astype(np.int16)
            self.set_chart_df_group()
            Print.Debug("更新坐标:{} 成功!".format(coord))
            return True
        except Exception as err:
            Print.Error(str(err))
            return False

    def get_unstack_data_to_csv_or_jmp_or_altair(
            self, test_id_list: List[int]
    ) -> (pd.DataFrame, dict):
        """
        作用是将to_chart_csv_data这样的堆叠数据加工成能让JMP理解的行列数据
        获取选取的测试数据 -> 用于统计分析
        如果 chart_df 是None 就用 df 中的数据
        :param test_id_list:
        :return:
            1. df
            2. calculation_capability
        """
        # if not test_id_list:
        #     raise Exception("get_unstack_data_to_csv_or_jmp_or_altair must have test_id")
        if self.to_chart_csv_data.chart_df is None:
            df = self.to_chart_csv_data.df
        else:
            df = self.to_chart_csv_data.chart_df
        name_dict = {}
        calculation_capability = {}
        for test_id in test_id_list:
            row = self.capability_key_dict[test_id]
            name_dict[test_id] = row[PTMD_HEAD.TEXT]
            calculation_capability[row[PTMD_HEAD.TEXT]] = row
        # 20230311->加入FailText

        temp_test_id_text_df = self.df_module.ptmd_df[[PTMD_HEAD.TEST_ID, PTMD_HEAD.TEXT]]
        temp_test_id_text_df.loc[len(temp_test_id_text_df)] = {PTMD_HEAD.TEST_ID: -1, PTMD_HEAD.TEXT: "-1:PASS"}
        df = pd.merge(df, temp_test_id_text_df, left_on=PRR_HEAD.FAIL_TEST_ID, right_on=PTMD_HEAD.TEST_ID)
        # rename -> key_id rename text
        df = df[GlobalVariable.JMP_SCRIPT_HEAD + test_id_list].copy()
        # {group}@{da_group}
        df[STDF_HEAD.ALL_GROUP] = df[STDF_HEAD.GROUP] + "@" + df[PRR_HEAD.DA_GROUP]
        if self.to_chart_csv_data.select_group is not None:
            df = df[df.ALL_GROUP.isin(self.to_chart_csv_data.select_group)]
        df = df.rename(columns=name_dict)  # TODO: 在其他地方, 这个就按照jmp_df来命名
        return df, calculation_capability

    def calculation_group(self, group_params: Union[list, None], da_group_params: Union[list, None]):
        """
        分组的制程能力报表, 并不适合在这里展示
        TxODO: future
        :param group_params:
        :param da_group_params:
        :return:
        """

    def update_limit(self, limit_new: Dict[int, Tuple[float, float, str, str]]):
        """
        limit_new[test_id] = (limit_min, limit_max, l_type, h_type)
        更新Limit并重新计算良率, 在 table ui 中进行
        1. 修改 ptmd_df 中的limit数据, 并确认 prr_df
        2. calculation_top_fail
        3. calculation_capability
        4. *show table
        TxODO: 注意会修改到原始表的数据, 20230124 Done
        :param limit_new:
        :return:
        """
        df = self.df_module.ptmd_df
        for index in range(len(df)):
            row: PtmdModule = df.iloc[index]
            row_test_id = row.TEST_ID
            if row_test_id not in limit_new:
                continue
            # 修改limit
            df.iloc[index, df.columns.get_loc(PTMD_HEAD.LO_LIMIT)] = limit_new[row_test_id][0]
            df.iloc[index, df.columns.get_loc(PTMD_HEAD.HI_LIMIT)] = limit_new[row_test_id][1]
            # 修改limit的类型
            opt_flag = row.OPT_FLAG
            parm_flag = row.PARM_FLG
            if limit_new[row_test_id][2] == LimitType.NoLowLimit:
                opt_flag = opt_flag | PtmdOptFlag.NoLowLimit  # opt_flag 0b1 << 6
            if limit_new[row_test_id][2] == LimitType.EqualLowLimit:
                opt_flag = opt_flag & ~ PtmdOptFlag.NoLowLimit
                parm_flag = parm_flag | PtmdParmFlag.EqualLowLimit
            if limit_new[row_test_id][2] == LimitType.ThenLowLimit:
                opt_flag = opt_flag & ~ PtmdOptFlag.NoLowLimit
                parm_flag = parm_flag & ~ PtmdParmFlag.EqualLowLimit
            if limit_new[row_test_id][3] == LimitType.NoHighLimit:
                opt_flag = opt_flag | PtmdOptFlag.NoHighLimit  # opt_flag 0b1 << 7
            if limit_new[row_test_id][3] == LimitType.EqualHighLimit:
                opt_flag = opt_flag & ~ PtmdOptFlag.NoHighLimit
                parm_flag = parm_flag | PtmdParmFlag.EqualHighLimit
            if limit_new[row_test_id][3] == LimitType.ThenHighLimit:
                opt_flag = opt_flag & ~ PtmdOptFlag.NoHighLimit
                parm_flag = parm_flag & ~ PtmdParmFlag.EqualHighLimit
            df.iloc[index, df.columns.get_loc(PTMD_HEAD.OPT_FLAG)] = opt_flag
            df.iloc[index, df.columns.get_loc(PTMD_HEAD.PARM_FLG)] = parm_flag

    def only_pass(self):
        prr = self.df_module.prr_df
        prr = prr[prr.FAIL_FLAG == FailFlag.PASS]
        self.df_module.prr_df = prr

    def calculation_new_top_fail(self):
        """
        计算新Limit的制程能力 -> 需要用到limit数据
        TxODO: future, 20230124 Done
        :return:
        """
        self.top_fail_dict = CapabilityUtils.calculation_new_top_fail(self.df_module)

    def screen_df(self, test_ids: List[int]):
        """
        只看选取的项目 ->TEST_ID isin 即可
        筛选: 底层数据也改为NewData, 数据拆除
        TxODO: future
        """
        ptmd_df = self.df_module.ptmd_df
        new_ptmd_df = ptmd_df[ptmd_df.TEST_ID.isin(test_ids)]
        self.df_module.ptmd_df = new_ptmd_df

    def drop_data_by_select_limit(self, func: str, limit_new: Dict[int, Tuple[float, float, str, str]]):
        """
        将选取测试项目limit内的数据删掉, 或是将limit外的数据删掉
        TODO: future.
            难度比较高, 逻辑先理清楚
            1. 先获取到所有的选取的测试项目
            2. 再逐项的筛选出Only Pass 或 Only Fail的数据
            3. 使用logic_and
        :param func: Union["inner", "outer"]
        :param limit_new:
        :return:
        """

    def verify_pass_have_nan(self) -> bool:
        """
        PASS的数据中含有空值! 不被允许, 检测的时候要定位到ID, 用来检测程序错误的
        1. 先在dtp中选取所有的PASS的数据, 使用DTP的FAIL_FLG==FailFlag.PASS
        2. 看所有的PASS的数据中的Result中是否还有nan这样的数据, 如果有的话测试程序就是有问题的
        TODO: 重要, 2023年需要添加完成
        """

    def verify_test_no_repetition(self) -> bool:
        """ 除了MPR外有重复的TEST_NO! 检测的时候要定位到ID, 用来检测程序不标准的 """

    def show_limit_diff(self):
        """
        显示导入的STDF见Limit之间的差异
        :return:
        """
        if self.df_module is None:
            return self.QStatusMessage.emit("请先将数据载入到数据空间中!")
        # TxODO:使用多进程后台处理(多进程可以实时修改代码,较为方便)
        p = Process(target=OpenXl.excel_limit_run, kwargs={
            'summary_df': self.select_summary,
            "limit_df": self.df_module.ptmd_df,
        })
        p.start()
        # OpenXl.excel_limit_run(self.select_summary, self.df_module.ptmd_df)

    def get_text_by_test_id(self, test_id: int):
        row = self.capability_key_dict[test_id]
        return row[PTMD_HEAD.TEXT]

    def update(self):
        """
        主要是可以更新Table界面上的制程能力报告, 比如limit更新后重新计算
        :return:
        """
        Print.Debug("update all QCalculation @emit")
        self.QCalculation.emit()

    def select_chart(self):
        """
        主要是用来选取绘图的数据, 在数据还是处于当前分组的一个状态下
        :return:
        """
        Print.Debug("select_chart QChartSelect @emit")
        self.QChartSelect.emit()

    def refresh_chart(self):
        """
        这个主要是数据有突然的变化, 比如分组改变了, 触发这个后把绘图的数据刷新重新绘图, 不Select
        :return:
        """
        Print.Debug("refresh_chart QChartRefresh @emit")
        self.QChartRefresh.emit()

    def refresh_limit(self):
        """
        主要触发Limit变化后刷新数据
        :return:
        """
        Print.Debug("refresh_limit QTableRefresh @emit")
        self.LimitRefresh.emit()


class FutureLi(Li):
    """
    加速验证库
    """

    def __init__(self):
        super(FutureLi, self).__init__()
        self.drop_text = PTMD_HEAD.TEXT
        self.part_flag = PartFlags.ALL
        self.bin_cache: dict = None
        self.skip_ftr: bool = False

    def set_drop_text(self, string: str):
        """

        """
        self.drop_text = string

    def set_skip_ftr(self, skip_ftr=False):
        self.skip_ftr = skip_ftr

    def set_group_part_flag(self, part_flag: int):
        """
        对TreeLoad的数据再做一次分组筛选
        """
        self.part_flag = part_flag

    def set_part_flag_inplace(self, part_flag: int, unit_group: str = None):
        """
        在这里就已经需要做什么ReadFail之类的操作了
        在UI中进行单元测试, UI中要先ByType, Web中就不用了
        """
        if part_flag == PartFlags.ALL:
            self.df_module.dtp_df.set_index([DTP_HEAD.NEW_TEST_ID, DTP_HEAD.DIE_ID], inplace=True)
            return
        if unit_group is None or unit_group == "":
            self.df_module.prr_df = ParserData.get_prr_data(
                self.df_module.prr_df, part_flag, ReadFail.Y
            )
        else:
            # 对summary进行group
            prr_df_list = []
            for key, df in self.select_summary.groupby(unit_group.split('|')):
                unit_prr_df = ParserData.get_prr_data(
                    self.df_module.prr_df[self.df_module.prr_df.ID.isin(df.ID)], part_flag, ReadFail.Y
                )
                prr_df_list.append(unit_prr_df)
            self.df_module.prr_df = pd.concat(prr_df_list)
        # self.df_module.dtp_df.reset_index(inplace=True)
        self.df_module.dtp_df = self.df_module.dtp_df[self.df_module.dtp_df.DIE_ID.isin(self.df_module.prr_df.index)]
        self.df_module.dtp_df.set_index([DTP_HEAD.NEW_TEST_ID, DTP_HEAD.DIE_ID], inplace=True)

    def concat(self) -> bool:
        """
        需要使用一个比较顺利的模型来做这个事情
        期望的模型:
            在Tree Load后, 有个PreView
        新的Concat做的事情:
            1. concat多个数据帧
            2. 去除Bin的重复并取最新的Bin和BinName
            3. prr_df给一个FAIL_TEST_ID的列, 用于缓存TopFail
            4. 给ptmd_df安排了一个TEXT唯一的NEW_TEST_ID
            5. dtp_df给一个用于识别是PASS还是FAIL的列 FAIL_FLG
            6. 将ptmd_df取最后一个TEXT去重为ptmd_df_limit
        """
        if len(self.id_module_dict) == 0:
            Print.Warning("未选取数据Tree, 无法整合Concat!")
            return False
        data_module_list = []
        for df_id, module in self.id_module_dict.items():
            if module is None:
                Print.Warning("有数据读取失败/缺失, ID:{}, 数据是None, 已经跳过, 是否因STDF中没有数据?".format(df_id))
                continue
            if len(module.prr_df) == 0:
                continue
            data_module_list.append(module)
        if len(data_module_list) == 0:
            Print.Warning("无有效数据, 无法整合Concat!")
            return False
        self.df_module = ParserData.new_contact_data_module(data_module_list, self.drop_text, self.skip_ftr)
        self.set_part_flag_inplace(part_flag=self.part_flag, unit_group="GROUP_TEXT")
        self.reload = True
        return True

    def calculation_top_fail(self):
        pass

    def gen_capability_key_dict(self):
        if self.capability_key_dict is None:
            self.capability_key_dict = dict()
        else:
            self.capability_key_dict.clear()
        for each in self.capability_key_list:
            self.capability_key_dict[each[PTMD_HEAD.TEST_ID]] = each  # capability_key_list算出来的Table数据都是按照TEST_ID作为key

    def calculation_capability(self):
        """
        1. 计算reject rate
        2. 计算cpk等
        :return:
        """
        self.capability_key_list = CapabilityUtils.new_calculation_capability(self.df_module)
        self.gen_capability_key_dict()

        if self.bin_cache is None:
            return
        for row_test_id, limit_new_tuple in self.bin_cache.items():
            if row_test_id not in self.capability_key_dict:
                continue
            capability = self.capability_key_dict[row_test_id]
            capability[PRR_HEAD.SOFT_BIN] = limit_new_tuple[4]
            capability["SOFT_BIN_NAME"] = limit_new_tuple[5]
            capability[PRR_HEAD.HARD_BIN] = limit_new_tuple[6]
            capability["HARD_BIN_NAME"] = limit_new_tuple[7]

    def calculation_new_top_fail(self):
        """
        计算新Limit的制程能力 -> 需要用到limit数据
        TxODO: future, 20230124 Done
        :return:
        """
        CapabilityUtils.new_re_calculation_capability(self.df_module, self.capability_key_dict)

    def get_select_calculation_capability(self, test_id_list: List[int]) -> dict:
        calculation_capability = {}
        for test_id in test_id_list:
            row = self.capability_key_dict[test_id]
            calculation_capability[row[PTMD_HEAD.TEXT]] = row
        return calculation_capability

    def get_group_data_log_to_txt(self) -> pd.DataFrame:
        """
        拉出DataLog
        """
        if self.to_chart_csv_data.chart_df is None:
            df = self.to_chart_csv_data.df
        else:
            df = self.to_chart_csv_data.chart_df
        return df[GlobalVariable.LOGGER_TXT_HEAD].copy()

    def get_unstack_data_to_csv_or_jmp_or_altair(
            self, test_id_list: List[int]
    ) -> (pd.DataFrame, dict):
        """
        作用是将to_chart_csv_data这样的堆叠数据加工成能让JMP理解的行列数据
        获取选取的测试数据 -> 用于统计分析
        如果 chart_df 是None 就用 df 中的数据
        :param test_id_list:
        :return:
            1. df
            2. calculation_capability
        """
        # if not test_id_list:
        #     raise Exception("get_unstack_data_to_csv_or_jmp_or_altair must have test_id")
        if self.to_chart_csv_data.chart_df is None:
            df = self.to_chart_csv_data.df
        else:
            df = self.to_chart_csv_data.chart_df
        name_dict = {}
        calculation_capability = {}
        for test_id in test_id_list:
            row = self.capability_key_dict[test_id]
            name_dict[test_id] = row[PTMD_HEAD.TEXT]
            calculation_capability[row[PTMD_HEAD.TEXT]] = row
        # 20230311->加入FailText

        temp_test_id_text_df = self.df_module.ptmd_df_limit[[PTMD_HEAD.NEW_TEST_ID, PTMD_HEAD.TEXT]].append(
            {PTMD_HEAD.NEW_TEST_ID: -1, PTMD_HEAD.TEXT: "-1:PASS"}, ignore_index=True
        )
        df = pd.merge(df, temp_test_id_text_df, left_on=PRR_HEAD.FAIL_TEST_ID, right_on=PTMD_HEAD.NEW_TEST_ID)
        # rename -> key_id rename text
        df = df[GlobalVariable.JMP_SCRIPT_HEAD + test_id_list].copy()
        # {group}@{da_group}
        all_group_column = df[STDF_HEAD.GROUP] + "@" + df[PRR_HEAD.DA_GROUP]
        df.insert(0, column=STDF_HEAD.ALL_GROUP, value=all_group_column)
        if self.to_chart_csv_data.select_group is not None:
            df = df[df.ALL_GROUP.isin(self.to_chart_csv_data.select_group)]
        df = df.rename(columns=name_dict)  # TODO: 在其他地方, 这个就按照jmp_df来命名
        return df, calculation_capability

    def update_limit(self, limit_new: Dict[int, Tuple[float, float, str, str, int, str, int, str]]):
        """
        limit_new[test_id] = (limit_min, limit_max, l_type, h_type)
        更新Limit并重新计算良率, 在 table ui 中进行
        1. 修改 ptmd_df_limit 中的limit数据, 并确认 prr_df
        2. calculation_top_fail
        3. calculation_capability
        4. *show table
        TxODO: 注意会修改到原始表的数据, 20230124 Done
        :param limit_new:
        :return:
        """
        df = self.df_module.ptmd_df_limit
        self.bin_cache = dict()
        for index in range(len(df)):
            row: PtmdModule = df.iloc[index]
            row_test_id = row.NEW_TEST_ID
            if row_test_id not in limit_new:
                continue
            limit_new_tuple = limit_new[row_test_id]
            # 修改limit
            df.iloc[index, df.columns.get_loc(PTMD_HEAD.LO_LIMIT)] = limit_new_tuple[0]
            df.iloc[index, df.columns.get_loc(PTMD_HEAD.HI_LIMIT)] = limit_new_tuple[1]
            capability = self.capability_key_dict[row_test_id]
            capability[PRR_HEAD.SOFT_BIN] = limit_new_tuple[4]
            capability["SOFT_BIN_NAME"] = limit_new_tuple[5]
            capability[PRR_HEAD.HARD_BIN] = limit_new_tuple[6]
            capability["HARD_BIN_NAME"] = limit_new_tuple[7]
            self.bin_cache[row_test_id] = limit_new_tuple
            # 修改limit的类型
            opt_flag = row.OPT_FLAG
            parm_flag = row.PARM_FLG
            if limit_new_tuple[2] == LimitType.NoLowLimit:
                opt_flag = opt_flag | PtmdOptFlag.NoLowLimit  # opt_flag 0b1 << 6
            if limit_new_tuple[2] == LimitType.EqualLowLimit:
                opt_flag = opt_flag & ~ PtmdOptFlag.NoLowLimit
                parm_flag = parm_flag | PtmdParmFlag.EqualLowLimit
            if limit_new_tuple[2] == LimitType.ThenLowLimit:
                opt_flag = opt_flag & ~ PtmdOptFlag.NoLowLimit
                parm_flag = parm_flag & ~ PtmdParmFlag.EqualLowLimit
            if limit_new_tuple[3] == LimitType.NoHighLimit:
                opt_flag = opt_flag | PtmdOptFlag.NoHighLimit  # opt_flag 0b1 << 7
            if limit_new_tuple[3] == LimitType.EqualHighLimit:
                opt_flag = opt_flag & ~ PtmdOptFlag.NoHighLimit
                parm_flag = parm_flag | PtmdParmFlag.EqualHighLimit
            if limit_new_tuple[3] == LimitType.ThenHighLimit:
                opt_flag = opt_flag & ~ PtmdOptFlag.NoHighLimit
                parm_flag = parm_flag & ~ PtmdParmFlag.EqualHighLimit
            df.iloc[index, df.columns.get_loc(PTMD_HEAD.OPT_FLAG)] = opt_flag
            df.iloc[index, df.columns.get_loc(PTMD_HEAD.PARM_FLG)] = parm_flag

    def screen_df(self, test_ids: List[int]):
        """
        只看选取的项目 ->NEW_TEST_ID isin 即可
        筛选: 底层数据也改为NewData, 数据拆除
        """
        ptmd_df_limit = self.df_module.ptmd_df_limit
        new_ptmd_df_limit = ptmd_df_limit[ptmd_df_limit.NEW_TEST_ID.isin(test_ids)]
        self.df_module.ptmd_df_limit = new_ptmd_df_limit

    def no_screen_df(self, test_ids: List[int]):
        """
        只看选取的项目 ! ->NEW_TEST_ID isin 即可
        筛选: 底层数据也改为NewData, 数据拆除
        """
        ptmd_df_limit = self.df_module.ptmd_df_limit
        new_ptmd_df_limit = ptmd_df_limit[~ptmd_df_limit.NEW_TEST_ID.isin(test_ids)]
        self.df_module.ptmd_df_limit = new_ptmd_df_limit

    def show_limit_diff(self):
        """
        显示导入的STDF见Limit之间的差异
        :return:
        """
        if self.df_module is None:
            return self.QStatusMessage.emit("请先将数据载入到数据空间中!")
        # TODO:使用多进程后台处理(多进程可以实时修改代码,较为方便)
        p = Process(target=OpenXl.excel_limit_run, kwargs={
            'summary_df': self.select_summary,
            "limit_df": self.df_module.ptmd_df,
        })
        p.start()

    def read_from_object(self, full_path):
        """
        从pickle中读取数据, 只显示到Table中
        """
        with open(full_path, 'rb') as file:
            pickle_dict_info = pickle.loads(file.read())
        with open(pickle_dict_info["capability_key_list"], 'rb') as capability_obj:
            self.capability_key_list = pickle.loads(capability_obj.read())
        df_module = {
            "prr_df": None,
            "dtp_df": None,
            "ptmd_df": None,
            "bin_df": None,
            "ptmd_df_limit": None,
        }
        for each in df_module.keys():
            if each not in pickle_dict_info:
                continue
            df_module[each] = pd.read_pickle(pickle_dict_info[each])
        self.df_module = DataModule(**df_module)
        self.gen_capability_key_dict()

    def save_object(self, full_path):
        """
        将数据保存到硬盘pickle中
        1. df_module
        2. capability_key_list
        """
        path, file_name = os.path.split(full_path)
        save_name = file_name[:file_name.rfind('.')]
        pickle_dict_info = {
            "capability_key_list": os.path.join(path, "{}_{}.pkl".format(save_name, "capability_key_list")),  # path
        }
        if not os.path.exists(path):
            os.makedirs(path)
        if self.df_module.prr_df is not None:
            save_path = os.path.join(path, "{}_{}.pkl".format(save_name, "prr_df"))
            pickle_dict_info["prr_df"] = save_path
            self.df_module.prr_df.to_pickle(save_path)
        if self.df_module.dtp_df is not None:
            save_path = os.path.join(path, "{}_{}.pkl".format(save_name, "dtp_df"))
            pickle_dict_info["dtp_df"] = save_path
            self.df_module.dtp_df.to_pickle(save_path)
        if self.df_module.ptmd_df is not None:
            save_path = os.path.join(path, "{}_{}.pkl".format(save_name, "ptmd_df"))
            pickle_dict_info["ptmd_df"] = save_path
            self.df_module.ptmd_df.to_pickle(save_path)
        if self.df_module.bin_df is not None:
            save_path = os.path.join(path, "{}_{}.pkl".format(save_name, "bin_df"))
            pickle_dict_info["bin_df"] = save_path
            self.df_module.bin_df.to_pickle(save_path)
        if self.df_module.ptmd_df_limit is not None:
            save_path = os.path.join(path, "{}_{}.pkl".format(save_name, "ptmd_df_limit"))
            pickle_dict_info["ptmd_df_limit"] = save_path
            self.df_module.ptmd_df_limit.to_pickle(save_path)
        with open(pickle_dict_info["capability_key_list"], 'wb') as capability_obj:
            data_pik = pickle.dumps(self.capability_key_list)
            capability_obj.write(data_pik)
        with open(full_path, 'wb') as file_obj:
            data_pik = pickle.dumps(pickle_dict_info)
            file_obj.write(data_pik)
        Print.Debug("SaveObject成功!")
